#!/usr/bin/perl -w

# TODO:
# COMPILER: WatcomC, IntelC
# CPU specification (+ MMX/SSE etc)
# ? add dependency: generated_file.mak : source_file.project => echo "Rebuild makefile!"+exit; insert into ALL target?
# ? custom targets: simply emit text to makefile?
# - multiline assignment (=, +=, sources()=) : make generic version (current: same code repeated few times)

=documentation

FILE FORMAT
~~~~~~~~~~~

1. Preprocessor:

word word word \
  continue line

!include filename

!if (perl-expression)
!ifdef var
!ifndef var
!else
!elif (expr)
!elifdef var
!elifndef (var)
!endif

!undef var
!message text
!dumpvars
!error text

WARNING: there is a side effect in (perl-expression): this expression can use vars
	from this (genmake) program (i.e. not defined inside processed script)

2. Variables (bash-like), expressions:

assignment:
	var = value
	var += value     same as [var = $var value]
multi-line assignment:
	var = {
		... text ... (many lines)
	}
	or "var += {" ....
substitution (r-value):
	$var
	${var}
defined(var)         will be replaced with 0 or 1

stdout capture:
	`command arg1 arg2`

3. System variables:

platform specification:
	PLATFORM	= win32 | win64 | cygwin | unix | unix32 | unix64 | osx
	COMPILER	= VisualC | GnuC
	TARGET		= vc-win32 | vc-win64 | mingw32 | cygwin | linux | linux32 | linux64 | osx
		Should specify either "TARGET" or "PLATFORM"+"COMPILER"
		"linux" = use current system's platform
		"linux32" or "linux64" = cross-compile for different platform

global:
	CPP_EXCEPT	= 1 == undef | 0		0 = disable C++ exceptions
	MAPFILES	= 1 | 0 == undef
	PDB			= 1 | 2 | 0 == undef	1 = link-level, 2 = compile-level, 0 = off (VisualC only)
	WINXP       = 1 | 0 == undef		explicitly support Windows XP platform
	STDLIBS		= list of used libraries (for "GNU Make" will append "lib" and will search in paths)
	LIBRARIES   = path(s) to additional library files
	LIBC		= static | shared == undef
	DEFFILE		= .def filename
	ALLOW_MISSING_INCLUDES = 1 | 0		when #include "filename" will fail, warning will be generated instead of error

per-target:
	CONSOLE		= 1 | 0 == undef		useless for Visual C++ - automatically detected by presence of main() or WinMain(),
										but used for Mingw32
	IMAGEBASE	= base address of the executable image
	LIBS		= list of used libraries (exact name, will not search in paths)
	IMPLIB		= filename				set filename of import library for "dynamic" target type; if not specified -
										- will not be created
	LINKFLAGS	= options				compiler-dependent command line options for linker

at a time of registration of source files
	DEFINES		= define1[=1] [define2 define3 ...]
	INCLUDES	= path1 [path2 path3 ...]
	OBJDIR		= directory for obj files (if not specified, obj files will be placed near c/cpp files)
	WARNLEVEL	= num					compiler warning level (0 - disable; 1 - min; 2 - more, max for VC=4, for gcc=2)
	OPTIMIZE	= size | speed | none
	INLINES		= no | explicit | all	expanding inlines: disabled | explicit only | any suitable
	OPTIONS		= string				compiler-dependent: extra options for command line
	CPP_OPTIONS	= string				same as OPTIONS, but applied only to C++ compiler

4. Commands:
	sources(NAME) = source files		add source files to list <NAME> with flags DEFINES,INCLUDES,OBJDIR;
										all items separated with spaces; item may be a wildcard
	target(type, outfile, NAME[, sym])	build target with type == {executable (exe), shared (dll/so), static (lib/a)}
										from objects, specified in list sources(NAME); NAME may contain multiple source lists
										separated with "+" (spaces are allowed); if sym specified, will be added symbolic
										name for a target; may specify a few symbolic targets, separated with spaces
	mkdir(directory)					create directory during build process
	push(var), pop(var)					save and restore value of the variable

5. Supported source files:
	*.asm		= NASM file
	*.rc		= resource script
	*.*			= C/C++ file

=cut

#------------------------------------------------------------------------------
#	Global configuration
#------------------------------------------------------------------------------

if (0) {			# C-like preprocessor
$PREPROC = '\#';
$COMMENT = "\/\/";
} else {			# make-like preprocessor
$PREPROC = '\!';
$COMMENT = '\#';
}


#------------------------------------------------------------------------------
#	Service output functions
#------------------------------------------------------------------------------

$S_RED     = "\033[1;31m";
$S_YELLOW  = "\033[1;33m";
$S_DEFAULT = "\033[0m";

sub Error {
	# add error text to generated makefile to prevent compilation
	print "#----------------------------------------\n";
	if (defined ($COMPILER) && $COMPILER eq "VisualC") {
		print "!error [GENMAKE ERROR] $_[0]\n";
	} else {
		# GNU make
		print "\$(error [GENMAKE ERROR] $_[0])\n";
	}
	print "#----------------------------------------\n\n";
	# log text to stderr
	die "${S_RED}ERROR: $_[0]${S_DEFAULT}\n";
}


sub Warning {
#	print "!message [GENMAKE WARNING] $_[0]\n";
	print STDERR "${S_YELLOW}WARNING: $_[0]${S_DEFAULT}\n";
}


%onceWarnings = ();

sub WarningOnce {
	my ($msg, $key) = @_;
	if (!exists($onceWarnings{$key})) {
		print STDERR "${S_YELLOW}WARNING: $_[0]${S_DEFAULT}\n";
		$onceWarnings{$key} = 1;
	}
}


# usage Splitter(message)
sub Splitter {
	my $spl = "#------------------------------------------------------------------------------\n";
	print $spl;
	my $msg = $_[0];
	if (defined($msg)) {
		print "#\t$msg\n$spl";
	}
	print "\n";
}


#------------------------------------------------------------------------------
#	Reading source file with simple preprocessing
#------------------------------------------------------------------------------

@includes  = ();
%vars      = ();
@var_stack = ();


sub ExpandVars {
	my ($line) = @_;
	return "" if !defined($line) || $line eq ""; # do not try to process empty line

	while (my ($var) = $line =~ /\bdefined\(\s*(\w+)\s*\)/) {
		my $val = 0;
		$val = 1 if exists($vars{$var});
		$line =~ s/ \bdefined \s* \( \s* $var \s*\) /$val/x;
	}

	while (my ($exec) = $line =~ /\`([^\`]+)\`/) {
		# grab console program's output
		my $out = qx/$exec/;
		$out =~ s/\n/ /g;
		$line =~ s/\`$exec\`/$out/;
	}

	return $line if $line !~ /\$/;				# line contains no vars

	#!! not fine parser: iterates over all variables instead of parsing of input string
	for my $key (keys(%vars)) {
		my $value = $vars{$key};
		$value = "" if !defined($value);
		$line =~ s/\$\b$key\b/$value/g;			# replace $var with value
		$line =~ s/\$\{$key\}/$value/g;			# replace ${var} with value
	}
	return $line;
}


sub PushVar {
	my $var = $_[0];
	push @var_stack, $vars{$var};
}


sub PopVar {
	my $var = $_[0];
	if (!@var_stack) {
		Error("pop $var with empty stack!");
	}
	$vars{$var} = pop @var_stack;
}


# preprocessor layer 0 -- comments, line continuation; will never return empty line
sub getline0 {
	my $line1 = "";
	while ($line = <IN>)
	{
		# remove CR/LF
		$line =~ s/\r//;
		$line =~ s/\n//;
		#!!! make optional:
#		if ($enabled) {
#			print "\033[0;32m+$line\033[0;37m\n";
#		} else {
#			print "\033[0;31m-$line\033[0;37m\n";
#		}
		# remove comments
		$line =~ s/\s*$COMMENT.*//;
# not needed if will squeeze spaces later
#		# remove trailing spaces
#		$line =~ s/\s*$//;
#		# remove leading spaces
#		$line =~ s/^\s*//;
		# line may be continued with "\"
		if ($line =~ /.*\\\s*$/) {
			my ($line2) = $line =~ /\s*(\S+|\S+.*\S+)\s*\\\s*$/;	# remove "\" at end of line and leading/trailing spaces
			$line1 .= "$line2 " if defined $line2;
		} else {
			$line = $line1.$line;
			# remove leading and trailing spaces (again)
			$line =~ s/^\s*//;
			$line =~ s/\s*$//;
			# replace all multiple spaces with a single one
			$line =~ s/\s\s+/ /g;
			# ignore empty lines
			next if $line eq "";
			return 1;
		}
	}
	Error ("unexpected \\ at end of file") if $line1 ne "";
	return 0;
}


$nestedIf  = 0;				# number of nested #if's with enabled input
$nestedIf0 = 0;				# number of nested #if's with disabled input
$enabled   = 1;				# 0 - disabled, 1 - enabled, 2 - waiting "endif" (disabled + ignore "else")

# usage: CheckCondition(cond, expression) -- cond = ""|"def"|"ndef" etc
sub CheckCondition {
	my ($type, $expr, $cmd) = @_;
	my $cond = 0;			# will set to 1 if input allowed

	if (!defined($type) || $type eq "") {
		# simple "if"
		$cond = eval ExpandVars($expr);
	} elsif ($type eq "def") {
		# ifdef
		$cond = 1 if exists $vars{$expr};
	} elsif ($type eq "ndef") {
		# ifndef
		$cond = 1 if !exists $vars{$expr};
	} else {
		Error ("unknown if... directive: $cmd");
	}
	if (!$cond) {
		$cond = 0;
	}
	return $cond;
}

# preprocessor layer 1 -- conditional parsing
sub getline1 {
	while (1) {
		# acquire line from the lower preprocessor level
		return 0 if !getline0();
		# analyze ...
		if ($line =~ /^$PREPROC\s*\w+/) {			# preprocessor directive
			# parse preprocessor
			my ($cmd, undef, $args) = $line =~ /^ $PREPROC\s*(\w+) (\s+ (\S+ (\s+ \S+)*)?)? \s* $/x;

			#-------- conditional directives ------------
			if ($cmd =~ /^if\w*/) {
				my ($type) = $cmd =~ /^if(\w+)$/;
				$nestedIf++;						# count of nested if's
				if ($enabled == 1) {
					my $cond = CheckCondition($type, $args, $cmd);
					if ($cond == 0) {
						$enabled = 0;
					}
				} else {
					$nestedIf0++;					# count of disabled if's
				}
			} elsif ($cmd =~ /^elif\w*/) {
				Error ("$cmd without if") if $nestedIf == 0;
				my ($type) = $cmd =~ /^elif(\w+)$/;

				if ($nestedIf0 == 0) {
					if ($enabled == 1) {
						$enabled = 2;				# enabled -> wait endif
					} elsif ($enabled == 0) {
						my $cond = CheckCondition($type, $args, $cmd);
						if ($cond != 0) {
							$enabled = 1;			# disabled -> enabled when cond<>0
						}
					}
				}
			} elsif ($cmd eq "else") {
				Error ("else without if") if $nestedIf == 0;
				if ($nestedIf0 == 0) {
					if ($enabled == 1) {
						$enabled = 2;				# enabled -> wait endif
					} elsif ($enabled == 0) {
						$enabled = 1;				# disabled -> enabled
					}
				}
			} elsif ($cmd eq "endif") {
				Error ("endif without if") if $nestedIf == 0;
				$nestedIf--;
				if ($nestedIf0 > 0) {
					$nestedIf0--;
				} else {
					$enabled = 1;
				}
			} elsif ($enabled == 1) {				# 0 or 2 - disabled
				# send line to next preprocessor level
				return 1;
			}
		} elsif ($enabled == 1) {
			# send line to next preprocessor level
			return 1;
		}
	}
}


# read lines until "}"
sub GetMultiLine {
	my $result = "";
	while (1) {
		if (!getline1 ()) {
			Warning ("opened \"{ ...\" multiline assignment");
			return $result;
		}
		# closing bracket
		return $result if $line eq "}";
		# append line
		next if $line eq "";						# avoid requirement of squeezing spaces
		if ($result eq "") {
			$result = $line;
		} else {
			$result .= " $line";
		}
	}
}


# preprocessor layer 2 (upper) -- other directives (incluse's, assignments etc)
sub getline {
	while (1) {
		if (!getline1 ()) {
			# return from included file
			if (!@includes) {
				# @includes array is empty
				Warning ("opened if/endif construction: $nestedIf") if $nestedIf;
				return 0;
			}
			close (IN);
			*IN = pop @includes;
			next;
		}

		if ($line =~ /^$PREPROC\s*\w+/) {			# preprocessor directive
			# parse preprocessor
			my ($cmd, undef, $args) = $line =~ /^ $PREPROC\s*(\w+) (\s+ (\S+ (\s+ \S+)*)?)? \s* $/x;
			#-------------- other directives ------------------
			if ($cmd eq "include") {
				# limit nested includes (and stop infinite recursion)
				Error ("too many nested includes") if $#includes > 16;
				local $file = ExpandVars($args);
				push @includes, *IN;
				local *IN2;							# NOTE: "local", not "my"
				open (IN2, $file) || Error ("cannot open include file \"$file\"");
				*IN = *IN2;
			} elsif ($cmd eq "undef") {
				delete $vars{$args};
			} elsif ($cmd eq "message") {
				print (STDERR ExpandVars($args)."\n");
			} elsif ($cmd eq "dumpvars") {
				for my $key (keys(%vars)) {
					print (STDERR "var:$key = $vars{$key}\n");
				}
			} elsif ($cmd eq "error") {
				Error (ExpandVars($args));
			} else {
				Error ("unknown preprocessor directive \"$cmd\" in line [$line]");
			}
		} elsif ($line =~ /^\w+\s*\=/) {
			# var = value
			my ($var, $value) = $line =~ /^\s*(\w+)\s*\=\s*(\S+.*)?/;
			$value = GetMultiLine() if defined($value) && $value eq "{";
			$vars{$var} = ExpandVars($value);		# can be "= eval $value" to evaluate arithmetic expression
		} elsif ($line =~ /^\w+\s*\+\=/) {
			# var += value
			my ($var, $value) = $line =~ /^\s*(\w+)\s*\+\=\s*(\S+.*)/;
			$value = GetMultiLine() if defined($value) && $value eq "{";
			if (exists($vars{$var})) {
				my $oldVar = $vars{$var};
				my $add    = ExpandVars($value);
				if (defined($oldVar)) {
					$vars{$var} = $oldVar." ".$add;
				} else {
					$vars{$var} = $add;
				}
			} else {
				$vars{$var} = ExpandVars($value);
			}
		} else {
			# ordinary line
			return 1;
		}
	}
}


#------------------------------------------------------------------------------
#	Compiler support
#------------------------------------------------------------------------------

sub InitCompiler {
	# retrieve global target configuration
	$COMPILER = $vars{"COMPILER"};
	$TARGET   = $vars{"TARGET"};
	$PLATFORM = $vars{"PLATFORM"};

	# process partial definitions
	if (defined($TARGET)) {
		# force COMPILER/PLATFORM
		if ($TARGET eq "vc-win32") {
			$COMPILER = "VisualC";
			$PLATFORM = "win32";
		} elsif ($TARGET eq "vc-win64") {
			$COMPILER = "VisualC";
			$PLATFORM = "win64";
		} elsif ($TARGET eq "mingw32") {
			$COMPILER = "GnuC";
			$PLATFORM = "win32";
		} elsif ($TARGET eq "cygwin") {
			$COMPILER = "GnuC";
			$PLATFORM = "cygwin";
		} elsif ($TARGET eq "linux") {
			$COMPILER = "GnuC";
			$PLATFORM = "unix";
		} elsif ($TARGET eq "linux32") {
			$COMPILER = "GnuC";
			$PLATFORM = "unix32";
		} elsif ($TARGET eq "linux64") {
			$COMPILER = "GnuC";
			$PLATFORM = "unix64";
		} elsif ($TARGET eq "osx") {
			$COMPILER = "GnuC";
			$PLATFORM = "osx";
		} else {
			Error ("Unknown TARGET: \"$TARGET\"");
		}
	} elsif (defined($COMPILER)) {
		if (defined($PLATFORM)) {
			#!! let COMPILER+PLATFORM to be specified from cmdline at the same time (no COMPILER=>PLATFORM overrides)
			Warning ("COMPILER and PLATFORM are both set; PLATFORM overrided\n");
		}
		# defaults for COMPILER
		if ($COMPILER eq "VisualC") {
			$PLATFORM = "win32";
		} elsif ($COMPILER eq "GnuC") {
			$PLATFORM = "unix";
		} else {
			Error ("Unknown COMPILER: \"$COMPILER\"");
		}
	} elsif (defined($PLATFORM)) {
		# defaults for PLATFORM
		if ($PLATFORM eq "win32" || $PLATFORM eq "win64") {
			$COMPILER = "VisualC";
		} elsif ($PLATFORM eq "cygwin") {
			$COMPILER = "GnuC";
		} elsif ($PLATFORM eq "unix" || $PLATFORM eq "unix32" || $PLATFORM eq "unix64" || $PLATFORM eq "osx") {
			$COMPILER = "GnuC";
		} else {
			Error ("Unknown PLATFORM: \"$PLATFORM\"");
		}
	} else {
		Error ("COMPILER/PLATFORM/TARGET is not set");
	}

	# update @vars hash to allow usage of vars from script
	$vars{"COMPILER"} = $COMPILER;
#	$vars{"TARGET"}   = $TARGET;		-- not modified
	$vars{"PLATFORM"} = $PLATFORM;

	# compiler definitions
	if ($COMPILER eq "VisualC") {
		$ObjFileExt = ".obj";
		$LibFileExt = ".lib";
	} elsif ($COMPILER eq "GnuC") {
		$ObjFileExt = ".o";
		$LibFileExt = ".a";
	} else {
		die;
	}
	if ($PLATFORM eq "unix" || $PLATFORM eq "unix32" || $PLATFORM eq "unix64" || $PLATFORM eq "osx") {
		$ExeFileExt = "";
		$DllFileExt = ".so";
	} elsif ($PLATFORM eq "win32" || $PLATFORM eq "win64" || $PLATFORM eq "cygwin") {
		$ExeFileExt = ".exe";
		$DllFileExt = ".dll";
	} else {
		die;
	}
}


sub EmitCompilerDefs {
	Splitter ("Compiler definitions");
	if ($COMPILER eq "VisualC") {
		# Windows XP support
		my $winxp = "";
		if (GetSrcOption("WINXP", 0) > 0) {
			# Microsoft: https://blogs.msdn.microsoft.com/vcblog/2012/10/08/windows-xp-targeting-with-c-in-visual-studio-2012/
			# Also: https://tedwvc.wordpress.com/2014/01/01/how-to-target-xp-with-vc2012-or-vc2013-and-continue-to-use-the-windows-8-x-sdk/
			$winxp = " -D _USING_V110_SDK71_";
		}
		# Platform defines
		my ($defs1, $defs2);
		if ($PLATFORM eq "win32") {
			$defs1 = " -D _WIN32 -D WIN32";
			$defs2 = " -d _WIN32 -d WIN32";
		} else {
			$defs1 .= " -D _WIN64 -D WIN64";
			$defs2 .= " -d _WIN64 -d WIN64";
		}
		# Compiler
		print "CPP  = cl.exe -nologo -c $defs1 -D _WINDOWS${winxp}\n";
		# Linker
		my $machine = ($PLATFORM eq "win64") ? "X64" : "X86";
		print "LINK = link.exe -nologo -filealign:512 -incremental:no -machine:$machine\n";
		# Library manager
		print "AR   = link.exe -lib -nologo\n";		# cannot use "LIB" name
		# Resource compiler
		print "RC   = rc.exe -nologo -dNDEBUG -l 0x409${winxp}${defs2}\n"
	} elsif ($COMPILER eq "GnuC") {
		my $platf = "gcc";
		# platform override
		if ($PLATFORM eq "win32") {
			# mingw32
			$platf .= " -mno-cygwin";
		} elsif ($PLATFORM eq "unix32") {
			$platf .= " -m32";
		} elsif ($PLATFORM eq "unix64") {
			$platf .= " -m64";
		}
		print "CPP  = \@$platf -pipe -c\n";
		print "LINK = \@$platf -pipe -s\n";
		print "AR   = \@ar -rcs\n";					# r=(replace files), c=(create archive), s=(write object file index)
	}
	print "\n";
}


#------------------------------------------------------------------------------
#	Source/object file list
#------------------------------------------------------------------------------
# array of object file info:
#	(source filename, obj dir, defines, include dirs, options, list name)
@sources = ();
# array of output directories
@outdirs  = ();		# real names
@outdirs2 = ();		# assigned variable name

# usage: GetSrcOption (opt_name[, default])
sub GetSrcOption {
	my $opt = $vars{$_[0]};
	return $opt if defined($opt);
	return $_[1] if defined $_[1];
	return "";
}

# fill outdirs and outdirs2, return variable name in a form $(varname)
sub ConvertObjdir {
	my $objdir = $_[0];

	return "" if !defined($objdir) || ($objdir eq "");

	# register output directory
	my $i = 0;
	for my $dir (@outdirs) {
		return "\$(".$outdirs2[$i].")" if $dir eq $objdir;
		$i++;
	}
	my $varname = ($i > 0) ? "OUT_".$i : "OUT";
#	print STDERR "$dir -> $varname\n";
	push @outdirs, $objdir;
	push @outdirs2, $varname;
	return "\$(".$varname.")";
}

# usage: AddObjectList (list_name, objects)
sub AddObjectList {
	my ($listName, $objs) = @_;
	my $defines  = GetSrcOption ("DEFINES");
	my $includes = GetSrcOption ("INCLUDES");
	my $objdir   = GetSrcOption ("OBJDIR");

	$objdir = ConvertObjdir ($objdir);

	# add objects
	ADD: for my $src (split (' ', $objs)) {
		# get options
		my $options = GetSrcOption ("OPTIONS");
		$options = " $options" if $options ne "";
		my $cpp_options = GetSrcOption("CPP_OPTIONS");
		$cpp_options = " $cpp_options" if $cpp_options ne "";
		if ($COMPILER eq "VisualC") {
			my $opt = GetSrcOption ("WARNLEVEL");
			# warning level
			$options .= " -W$opt" if $opt ne "";
			# optimization strategy
			$opt = GetSrcOption ("OPTIMIZE");
			if ($opt eq "size") {
				$options .= " -O1";
			} elsif ($opt eq "speed") {
				$options .= " -O2";
			} elsif ($opt eq "none") {
				$options .= " -Od";
			} elsif ($opt ne "") {
				WarningOnce ("unknown OPTIMIZE option: [$opt]");
			}
			# inline expansion
			$opt = GetSrcOption ("INLINES");
			if ($opt eq "no") {
				$options .= " -Ob0";
			} elsif ($opt eq "explicit") {
				$options .= " -Ob1";
			} elsif ($opt eq "all") {
				$options .= " -Ob2";
			} elsif ($opt ne "") {
				WarningOnce ("unknown INLINES option: [$opt]");
			}
			# C++ exceptions
			if ($src =~ /\.cpp$/) {
				$opt = GetSrcOption ("CPP_EXCEPT");
				if ($opt eq "1" || $opt eq "") {
					$options .= " -EHsc";	# old compilers: "-GX"
				} elsif ($opt eq "0") {
					$options .= " -EHs-";	# old compilers: "-GX-"
				} else {
					WarningOnce ("unknown CPP_EXCEPT option: [$opt]");
				}
				# append common C++ options
				$options .= $cpp_options;
			}
			# debugging
			$opt = GetSrcOption("PDB", 0);
			if ($opt >= 2) {
				# NOTE: using -Zi will not allow parallel compilation of multiple source files with VS2013+
#??				$options .= " -Zi -Fd\"$objdir/\"";	# enable debugging information; note: no final slash, so pdb will be names as objdir
				$options .= " -Z7";						# enable debugging information stored in OBJ file
			}
		} elsif ($COMPILER eq "GnuC") {
			my $opt = GetSrcOption ("WARNLEVEL");
			# warning level: GnuC can disable warnings or enable all warnings
			if ($opt ne "") {
				if ($opt eq "0") {
					$options .= " -w";					# disable warnings
				} elsif ($opt eq "1") {
					$options .= " -Wall";				# "all" but not "extra" ...
				} else { #if ($opt eq "2")
					$options .= " -Wall -Wextra";		# max warning level
				}
			}
			# optimization strategy
			$opt = GetSrcOption ("OPTIMIZE");
			if ($opt eq "size") {
				$options .= " -Os";
			} elsif ($opt eq "speed") {
				$options .= " -O3";
			} elsif ($opt ne "") {
				Warning ("unknown OPTIMIZE option: [$opt]");
			}
			# inline expansion
			$opt = GetSrcOption ("INLINES");
			if ($opt eq "no") {
				$options .= " -fno-inline";
			} elsif ($opt eq "explicit") {
				$options .= " -finline-functions";	#??
			} elsif ($opt eq "all") {
				$options .= " -finline-functions";
			} elsif ($opt ne "") {
				Warning ("unknown INLINES option: [$opt]");
			}
			# C++ exceptions
			if ($src =~ /\.cpp$/) {
				$opt = GetSrcOption ("CPP_EXCEPT");
				if ($opt eq "1" || $opt eq "") {
					# use compiler defaults
				} elsif ($opt eq "0") {
					$options .= " -fno-rtti -fno-exceptions";
				} else {
					WarningOnce ("unknown CPP_EXCEPT option: [$opt]");
				}
				# append common C++ options
				$options .= $cpp_options;
			}
		} else {
			die "unknown compiler";
		}
		# check for some errors/warnings
		for my $item (@sources) {
			my ($src2, $objdir2, $defines2, $includes2, $options2, $listName2) = split('%', $item);
			next if $src2 ne $src;
			# check item duplicates
			if ($listName2 eq $listName) {
				Warning ("file \"$src\" already included in list \"$listName\"");
				next ADD;
			}
			# check for presence of current source file with a same output dir,
			# but with a different options
			Error "file \"$src\" compiled into directory \"$objdir\" included in lists \"$listName\" and \"$listName2\" with a different options"
#			. "\nobjdir{$objdir2,$objdir} defs{$defines2,$defines} incl{$includes2,$includes} opt{$options2,$options}\n"
				if ($objdir2 eq $objdir && ($defines2 ne $defines || $includes2 ne $includes || $options2 ne $options));
		}
		# register source file
		push @sources, join('%', $src, $objdir, $defines, $includes, $options, $listName);
	}
}

# usage: GetObjFilename (source_name, obj_dir)
sub GetObjFilename {
	my ($src, $objdir) = @_;
	my $ext = $ObjFileExt;
	# replace source extension
	my ($srcType) = $src =~ /\S+\.([^\.\s]+)$/;
	$ext = ".res" if $srcType eq "rc" && $COMPILER eq "VisualC"; # .rc -> .res file (for VisualC only)
	my ($obj) = ($src =~ /^(\S+)\.\w+$/)[0].$ext;
	# replace output directory
	if ($objdir ne "") {
		$obj =~ s/^.*\///;
		$obj = $objdir."/".$obj;
	}
	return $obj;
}

# usage: AppendFilePath (filename, path)
sub AppendFilePath {
	my ($file, $path) = @_;
	return $file if $path eq "";			# path is ""
	$file =~ s/\\/\//g;						# replace "\" in filename with "/"
	$path .= "/" if $path !~ /\/$/;			# ensure "/" at the end of path
	$file = $path.$file;					# append path
	# compress path: remove "word/../"
	# NOTE: $inc =~ s/\b\w+\/\.\.\///g; -- does not works when dir name containing "." (\b will work incorrect)
	$file =~ s/^[^\/\.]+\/\.\.\///g;		# at begin of string
	$file =~ s/\/[^\/\.]+\/\.\.//g;			# in the middle of the string
	# remove "./" at filename start
	$file =~ s/^\.\///;
	return $file;
}

# usage: GetObjectList(list_name)
# accepts single object list name
sub GetObjectList {
	my $listName = $_[0];
	my $list = "";
	# output files
	for my $item (@sources) {
		my ($src, $objdir, undef, undef, undef, $lst) = split('%', $item);
		$list .= " \\\n\t".GetObjFilename ($src, $objdir) if $lst eq $listName;
	}
	Error ("no files in object list \"$listName\"") if $list eq "";
	return $list;
}

# usage: GetDirsList(obj_list_name)
# accepts list of object list names
sub GetDirsList {
	my $objListName = $_[0];
	my $list = "";
	my %dirs = ();
	# output files
	for my $item (@sources) {
		my (undef, $objdir, undef, undef, undef, $lst) = split('%', $item);
		for my $listName (split(',', $objListName)) {
			if ($lst eq $listName) {
				if (!exists($dirs{$objdir})) {
					$list .= " ".$objdir;
					$dirs{$objdir} = 1;
					last;
				}
			}
		}
	}
	return $list;
}

# function similar to ExpandVars()
sub ExpandWildcards {
	my ($line) = @_;
	return "" if !defined($line) || $line eq ""; # do not try to process empty line

	return $line if $line !~ /\*/;				# line contains no wildcards

	my $line2 = "";
	for my $src (split (' ', $line)) {
		my $srcInfo = $src;
		if ($src !~ /\*/) {
			$line2 .= " $src";
			next;
		}
		$src =~ s/\\/\//g;			# '\' -> '/'
		# get path, if exists
		my (undef, $path, $mask) = $src =~ /((.*)\/)?([^\/]+)/;
		# normalize and convert mask
		$mask =~ s/\./\\./g;		# '.' -> '\.'
		$mask =~ s/\*/\.\*/g;		# '*' -> '.*'
		$mask = "^".$mask."\$";		# add ^ at start and $ at end
		$path = "." if !defined($path);
		# scan directory
		opendir (DIR, $path) or Error ("Directory \"$path\" was not found");
		my @filelist = readdir (DIR);
		closedir (DIR);
		# case-insensitive sort of files (for predictable behaviour)
		@filelist = sort {uc($a) cmp uc($b)} @filelist;
		# check files
		my $found = 0;
		for my $f (@filelist) {
			if (-f "$path/$f" && ($f =~ $mask)) {
				if ($path ne ".") {
					$line2 .= " $path/$f";
				} else {
					$line2 .= " $f";
				}
				$found = 1;
			}
		}
		Warning ("No files for wildcard \"$srcInfo\"") if !$found;
	}
	# remove leading space
	$line2 =~ s/^\ //;
	return $line2;
}


#------------------------------------------------------------------------------
#	Generating compiler commands
#------------------------------------------------------------------------------

%depends = ();

sub CollectFileDependencies {
	my ($file, $includes, $srcType, $chain) = @_;
	my $dep = $depends{$file};
	if (defined($dep)) {
		return 0 if $dep ne "(computing)";		# already finished
		return -1;								# recurse to this file: will display warning
	}
	$chain = $file if !defined($chain);			# used for error reporting

	$dep = "";
	$depends{$file} = "(computing)";			# flag for detection of recursive includes
	my ($path) = $file =~ /^(\S+)\/[\w+\-\.]+$/;
	$path = "" if !defined($path);

	local *DEP;									# NOTE: "local", not "my"
	open (DEP, $file) || Error ("cannot open file \"$file\"");	# headers should be already checked, but .c/.cpp - may absent
	while (my $line = <DEP>) {
		my ($tmp, $inc);
		my $doRecurse = 1;
		if ($srcType eq "asm") {
			# [%include "file"] or [incbin "file"]
			($tmp, $inc) = ($line =~ /^\s* ( \%\s* include | incbin ) \s* [\"\'] ([\w\.]+) [\"\']/x);
			if (defined($tmp) && $tmp eq "incbin") {
				$doRecurse = 0;
			}
		} else {
			# [#include "file"]
			$inc = ($line =~ /^\s* \# \s* include \s* \" ([\w\.\-\\\/]+) \" \s* (\/\/.*|\/\*.*)? [\r\n]* $/x)[0];
		}
		next if !defined($inc);
		# find included file in current directory or in INCLUDES paths
		my $inc2 = AppendFilePath ($inc, $path);
		my $found = 0;

		$found++ if -f $inc2;

		if ($includes ne "") {
			# INCLUDES is not empty ...
			for my $path2 (split(" ", $includes)) {
				next if $path eq $path2;		# this path was already checked
				my $inc3 = AppendFilePath ($inc, $path2);
				next if $inc3 eq $inc2;			# file with appended path is known ...
				if (-f $inc3) {
					if ($inc !~ /.*\.\..*/) {
						# file has no ".." in path, warning if found in multiple places
						WarningOnce ("file \"$inc\" were found in a few places; INCLUDES=[$includes]", "many:$inc:$includes") if $found != 0;
					}
					$found++;
					$inc2 = $inc3;
				}
			}
		}

		if (!$found) {
			my $doWarn = $vars{"ALLOW_MISSING_INCLUDES"};
			if (defined($doWarn) && $doWarn) {
				Warning ("cannot find file \"$inc\" included from \"$chain\", directories used: [$includes]") unless $found;
			} else {
				Error ("cannot find file \"$inc\" included from \"$chain\", directories used: [$includes]") unless $found;
			}
		} else {
			# check file presence
			if ($doRecurse) {
				if (CollectFileDependencies ($inc2, $includes, $srcType, $chain."->".$inc2) == -1) {
					# may be inc<->file, and may be file1->file2->file3->file1 ...
					WarningOnce ("circular reference from \"$inc2\" to \"$chain\"", "circ:$file<->$inc2");
				}
			} else {
				$depends{$inc2} = "";
			}
			# remember file
			$dep .= " $inc2 ";					# spaces around filename
		}
	}
	close (DEP);
	$depends{$file} = $dep;
	return 1;
}


sub GatherDependInfo {
	my $item;
	# get dep info for source files
	for $item (@sources) {
		my ($src, undef, undef, $includes) = split('%', $item);
		my ($srcType) = $src =~ /\S+\.(\w+)$/;
		CollectFileDependencies ($src, $includes, $srcType);
	}
#	print "##################\n";
#	for $item (keys(%depends)) { print "$item -> $depends{$item}\n" }
#	print "##################\n";
	# append included files dependency info to a source files info
	while (1) {
		my $found = 0;
		for $item (keys(%depends)) {
			my $depLine = $depends{$item}; 				# all dependencies of file $item
			my $found2 = 0;
			for my $dep (split (" ", $depLine)) {		# single dependant
				for my $dep2 (split (' ', $depends{$dep})) { # check linked dependencies
					if ($depLine !~ /\s $dep2 \s/x) {	# this file not yet in list
						$depLine .= " $dep2 ";
						$found++;
						$found2++;						# flag to update $depLine
					}
				}
			}
			$depends{$item} = $depLine if $found2;
		}
		last if !$found;								# all linked dependencies processed
	}
#	for $item (keys(%depends)) { print "$item -> $depends{$item}\n" }
#	print "##################\n";
	# sort file dependencies
	for $item (keys(%depends)) {
		$depends{$item} = join(" ", sort split(" ", $depends{$item}));
	}
}

# usage: GenerateOptions(string_with_spaces, option)
# will return "option string1 option string2 ..." (no spaces between option ans string!)
sub GenerateOptions {
	my ($str, $opt) = @_;
	my $line = "";
	for my $item (split (" ", $str)) {
		$line .= " $opt$item";
	}
	return $line;
}


# simple makefile size optimization
$lastDepends = "";
$lastOptions = "";
$dependCount = 0;
$dependVar   = "";		# current dependency variable
$optionVar   = "";		# current options variable
%optionNames = ();		# hash: options -> variable name
%optionNmCt  = ();		# hash: obj list name -> num (check existence of name)

# usage: FlushObjectFile (src, obj_dir, defines, includes, list_name)
# will generate makefile lines to compile single object file
sub FlushObjectFile {
	my ($srcFile, $objDir, $defines, $includes, $options, $objListName) = @_;
	my $objFile = GetObjFilename($srcFile, $objDir);
	my ($srcType) = $srcFile =~ /\S+\.(\w+)/;
	my ($srcDir, $srcCleanName) = $srcFile =~ /(\S+\/)?([^\/]+)$/;

	# linker-only files - GetObjFilename() returns $srcFile
	if ($srcFile eq $objFile) {
		return;
	}

	# compute additional options (defines and includes)
	$options .= GenerateOptions ($defines, "-D ") if $defines ne "";
	$options .= GenerateOptions ($includes, "-I ") if $includes ne "";
	if (($options ne "") && ($lastOptions ne $options)) {
		# options was changed - update variable
		$lastOptions = $options;
		if (exists($optionNames{$options})) {
			$optionVar = $optionNames{$options};
		} else {
			# these options was already declared before - reuse
			my $optionCount = 0;
			$optionCount = $optionNmCt{$objListName} if exists($optionNmCt{$objListName});
			$optionCount++;
			$optionVar = "OPT_$objListName";
			$optionVar .= "_$optionCount" if $optionCount > 1;
			# remember options
			$optionNames{$options}    = $optionVar;
			$optionNmCt{$objListName} = $optionCount;
			print "$optionVar =$options\n\n";
		}
	}

	# flush dependency info
	my $dep = $depends{$srcFile};
	if (($dep ne "") && ($lastDepends ne $dep)) {
		# dependency info was changed - update variable
		$lastDepends = $dep;
		$dependCount++;
		$dependVar = "DEPENDS";
		if ($COMPILER eq "GnuC") {
			# Gnu MAKE utility support
			$dependVar .= "_$dependCount";
		}
		# put dependency info
		print "$dependVar =";
		for my $item (split (" ", $dep)) {
			print " \\\n\t$item";
		}
		print "\n\n";
	}

	print "$objFile : $srcFile";
	print " \$($dependVar)" if $dep ne "";
	print "\n";

	my $line = "";
	if ($COMPILER eq "VisualC") {
		if ($srcType eq "asm") {
			my $tgt = "win32";
			$tgt = "win64" if $PLATFORM eq "win64";
			$line = "nasm.exe -f $tgt ".GenerateOptions($includes, "-I")." -o \"$objFile\"";
		} elsif ($srcType eq "rc") {
			my $inc = "";
			my $defs = "";
			$inc = " ".GenerateOptions ($includes, "-i ") if $includes ne "";
			$defs = " ".GenerateOptions ($defines, "-d ") if $defines ne "";
			$srcDir = "." if $srcDir eq "";		# empty $srcDir means current directory
			$line = "\$(RC) -i ${srcDir}${inc}${defs} -fo\"$objFile\"";
		} else {
			# C/C++ file
			my $crt = GetSrcOption ("LIBC");
			$line = "\$(CPP)";
			if ($crt eq "static") {
				$line .= " -MT";	# static multithreaded CRT; use -ML for single-threaded ?
			} elsif ($crt eq "shared" || $crt eq "") {
				$line .= " -MD";
			} else {
				Error ("unknown LIBC type: $crt");
			}
			#?? VC std defines: _LIB, NDEBUG, _DEBUG
			$line .= " \$($optionVar)" if $options ne "";
			$line .= " -Fo\"$objFile\"";
		}
		$line .= " $srcFile";
	} elsif ($COMPILER eq "GnuC") {	#!! GnuC - other
		# We're suppressing command line echo with '@', so echo file name ourselves
		$line .= "\@echo $srcCleanName\n\t";
		if ($srcType eq "asm") {
			my $tgt = "elf32";
			$tgt = "elf64" if $PLATFORM eq "unix64";
			$tgt = "coff" if $PLATFORM eq "win32";
			$line .= "nasm -f $tgt ".GenerateOptions($includes, "-I")." -o \"$objFile\"";
		} elsif ($srcType eq "rc") {
			$line .= "windres -O coff -I $srcDir -o $objFile";
		} else {					#!! GnuC - compiler
			# C/C++ file
			#?? GnuC/Linux: static/shared LIBC
			$line .= "\$(CPP)";
			$line .= " \$($optionVar)" if $options ne "";
			$line .= " -o $objFile";
		}
		$line .= " $srcFile";
	} else {
		die "unknown compiler";
	}

	print "\t$line\n\n";
}


#------------------------------------------------------------------------------
#	Targets
#------------------------------------------------------------------------------
# targets array:
#	(symbolic name, output file, type, objListName, targetDir)
@targets = ();
# target options:
#	{filename:option} = value
%targetOptions = ();

# usage: SetDefaultExtension ("file1 file2 file3 ...", extension)
sub SetDefaultExtension {
	my ($names, $ext) = @_;
	return "" if $names eq "";
	return $names if $ext eq "";

	my $line = "";
	for my $file (split(" ", $names)) {
		$file .= $ext if $file !~ /[\/\w]+\.\w+/;
		if ($line ne "") {
			$line .= " $file";
		} else {
			$line = $file;
		}
	}
	return $line;
}

# usage: TargetOption (targetName, optName, remove, AddExt)
sub SetTargetOption {
	my ($targetName, $optName, $remove, $ext) = @_;
	$remove = 0 if !defined($remove);
	my $value = $vars{$optName};
	if (defined($value)) {
		$value = SetDefaultExtension ($value, $ext) if defined $ext;
		$targetOptions{$targetName.":".$optName} = $value;
		delete $vars{$optName} if $remove == 1;
	}
}

# usage: GetTargetOption (targetName, optName)
sub GetTargetOption {
	my ($targetName, $optName) = @_;
	my $n = $targetOptions{$targetName.":".$optName};
	return $n if defined $n;
	return "";
}

# usage: RememberTarget (SymbolicName, OutFile, type, ObjListName, TargetDirectory)
# store target info into @targets array
sub RememberTarget {
	my ($symName, $n, $type, $objListName, $targetDir) = @_;
	# append default extension for output file
	if ($type eq "executable") {
		$n = SetDefaultExtension ($n, $ExeFileExt);
	} elsif ($type eq "shared") {
		$n = SetDefaultExtension ($n, $DllFileExt);
	} elsif ($type eq "static") {
		$n = SetDefaultExtension ($n, $LibFileExt);
	} else {
		Error ("unknown target: \"$type\"");
	}
	# remember options
	SetTargetOption ($n, "MAPFILES");
	if ($COMPILER eq "VisualC") {
		SetTargetOption ($n, "STDLIBS", 0, $LibFileExt);
	} else {
		# no lib extension
		SetTargetOption ($n, "STDLIBS", 0);
	}
	SetTargetOption ($n, "LIBS", 1, $LibFileExt);
	SetTargetOption ($n, "LIBRARIES");
	SetTargetOption ($n, "LINKFLAGS");
	# some options will be cleared after checking
	SetTargetOption ($n, "CONSOLE",   1);
	SetTargetOption ($n, "IMAGEBASE", 1);
	SetTargetOption ($n, "IMPLIB",    1, $LibFileExt);
	SetTargetOption ($n, "DEFFILE",   1);
	# remember target command
	push @targets, join('%', $symName, $n, $type, $objListName, $targetDir);
}


# usage: FlushTargetText (target={executable,shared,static}, outfile, objlist, haveDirs)
# generate makefile lines to link target file (exe, dll, lib)
sub FlushTargetText {
	my ($target, $n, $objListName, $dirList) = @_;
	my $line;

	# cut file extension
	my ($shortOutName) = $n =~ /^(.*?)(\.\w+)?$/;	# strip extension
	# get used libraries
	my $libs     = GetTargetOption ($n, "LIBS");
	my $stdlibs  = GetTargetOption ($n, "STDLIBS");
	my $libpath  = GetTargetOption ($n, "LIBRARIES");

	my $baseaddr = GetTargetOption ($n, "IMAGEBASE");

	my $fileList = "";
	for my $listName (split(',', $objListName)) {
		$fileList .= " \$(${listName}_FILES)";
	}

	# build target dependency line
	my $prefix = "$n :";
	$prefix .= "$dirList" if $dirList ne "";	# directories
	$prefix .= "$fileList";						# files
	# check dependency by libs from another targets
	for my $lib (split(" ", $libs)) {
		for my $t (@targets) {
			my (undef, $file) = split ('%', $t);
			if (($file eq $lib) || (GetTargetOption ($file, "IMPLIB") eq $lib)) {
				$prefix .= " $file";
				Warning ("target \"$n\" depends with library \"$lib\" on self") if $file eq $n;
			}
		}
	}
	my $deffile = GetTargetOption ($n, "DEFFILE");
	if ($deffile ne "") {
		$prefix .= " $deffile";
	}
	$prefix .= "\n";

	if ($target eq "executable") {
		$prefix .= "\t\@echo Linking $n\n";
	} elsif ($target eq "shared") {
		$prefix .= "\t\@echo Linking dynamic library $n\n";
	} elsif ($target eq "static") {
		$prefix .= "\t\@echo Creating static library $n\n";
	}

	# NOTE: this logic may be implemented with our preprocessor (exec some script for each target)
	#-------------- Visual C++ options --------------------
	if ($COMPILER eq "VisualC") {
		if ($target eq "static") {
			return $prefix."\t\$(AR) -out:\"$n\"$fileList";
		}
		$line = "\t\$(LINK) -out:\"$n\"";
		$line .= GenerateOptions ($libpath, "-libpath:") if $libpath ne "";
		$line .= " $stdlibs" if $stdlibs ne "";
		$line .= " $libs" if $libs ne "";
		$line .= " -base:0x$baseaddr" if $baseaddr ne "";
		$line .= " -map:\"$shortOutName.map\"" if GetTargetOption ($n, "MAPFILES") eq "1";
		$line .= " -debug -pdb:\"$shortOutName.pdb\" -opt:ref -opt:icf" if GetSrcOption("PDB", 0) >= 1;
		$line .= $fileList;
		my $linkflags = GetTargetOption ($n, "LINKFLAGS");
		$line .= " $linkflags" if ($linkflags ne "");
		if (GetTargetOption ($n, "CONSOLE") eq "1") {
			$line .= " -subsystem:console";
		} else {
			$line .= " -subsystem:windows";
		}
		if (GetSrcOption("WINXP", 0) > 0) {
			# https://blogs.msdn.microsoft.com/vcblog/2012/10/08/windows-xp-targeting-with-c-in-visual-studio-2012/
			# append OS version to "subsystem" option
			$line .= ($PLATFORM eq "win64") ? ",5.02" : ",5.01";
		}
		if ($target eq "executable") {
			return $prefix.$line;
		} elsif ($target eq "shared") {
			$line .= " -dll";
			if ($deffile ne "") {
				$line .= " -def:\"$deffile\"";
			}
			my $implib = GetTargetOption ($n, "IMPLIB");
			if ($implib ne "") {
				$line .= " -implib:\"$implib\"";
			} else {
				# if IMPLIB is not specified, delete unneeded linker output
				$line .= " -implib:\"delete.lib\"";
				$line .= "\n\tdel delete.lib\n\tdel delete.exp";
			}
			return $prefix.$line;
		} else {
			Error ("unknown target: \"$target\"");
		}
	#--------------- Gnu C Compiler -----------------------
	} elsif ($COMPILER eq "GnuC") {		#!! GnuC - linker
		$line = "\t\$(LINK) -o $n";
		$line .= $fileList;
		my $linkflags = GetTargetOption ($n, "LINKFLAGS");
		$line .= " $linkflags" if ($linkflags ne "");
		# static/shared libgcc
		my $crt = GetSrcOption ("LIBC");
		if ($crt eq "static") {
			$line .= " -static-libgcc";
		} elsif ($crt eq "shared" || $crt eq "") {
			$line .= " -shared-libgcc";
		} else {
			Error ("unknown LIBC type: $crt");
		}
		$line .= GenerateOptions ($libpath, "-L") if $libpath ne "";
		$line .= GenerateOptions ($stdlibs, "-l") if $stdlibs ne "";
		$line .= " $libs" if $libs ne "";
		if ($PLATFORM eq "win32") {
			my $cons = GetTargetOption ($n, "CONSOLE");
			$line .= " -mwindows" if ($cons eq "") || ($cons eq "0");
			# win32-specific options
			$line .= " -Wl,--image-base,0x$baseaddr" if $baseaddr ne "";
		}
		#?? can add "--demangle" to linker cmdline, but does not works with gcc4 ...
		$line .= " -Wl,-Map,$shortOutName.map" if GetTargetOption ($n, "MAPFILES") eq "1";
		if ($target eq "executable") {
			return $prefix.$line;
		} elsif ($target eq "shared") {
			$line .= " -shared";
			#?? IMPLIB for GnuC
			return $prefix.$line;
		} elsif ($target eq "static") {
			return $prefix."\t\$(AR) ${n}${fileList}";
#		} else {
#			Error ("unknown target: \"$target\"");
		}
	#--------------- other compilers ----------------------
	} else {
		die "unknown compiler";
	}
}

sub GenMkdir {
	my $dir = $_[0];
	if ($COMPILER eq "VisualC") {
		# Win32 mkdir have no "-p" option
		return "if not exist \"$dir\" mkdir \"$dir\"";
	} else {
		return "\@mkdir -p $dir";
	}
}


#------------------------------------------------------------------------------
# analyze command line

if (!@ARGV) {
	print STDERR "Usage: genmake <project file> [var=value var=value ...]\n";
	exit;
}
$INNAME = $ARGV[0];
shift;
for $item (@ARGV) {
	my ($name, $value) = $item =~ /(\w+)=(\S*)/;
	Error ("invalid command line argument: [$item]") if !defined $name;
	$vars{$name} = $value;
}

print STDERR "Generating makefile from $INNAME ...\n";

# allow script to check platform/compiler variables (but, this will not allow to change
# this vars from script ...)
InitCompiler ();

# open source file
open (IN, $INNAME) || Error ("can't open infile $INNAME");
# parse source
while (getline ())
{
	$line = ExpandVars ($line);
	if ($line =~ /^sources\(\s*\w+\s*\)\s*\=/) {
		my ($objListName, $srcFiles) = $line =~ /\(\s*(\w+)\s*\)\s*\=\s*(\S+(.*\S+)?)$/;
		$srcFiles = GetMultiLine() if defined($srcFiles) && $srcFiles eq "{";
		$srcFiles = ExpandVars($srcFiles);
		$srcFiles = ExpandWildcards($srcFiles);
		AddObjectList ($objListName, $srcFiles);
	} elsif ($line =~ /^target\(.*\)$/) {
		my ($type, $outfile, $objListName, undef, undef, $symName) = $line =~
			/\( \s* (\w+) \s*,\s* (\S+) \s*,\s* (\w+([\w\s\+]*\w)?) (\s*,\s*(\w+([\s\w]*\w+)?))? \s*\)/x;
		$objListName = join(',', split('\s*\+\s*', $objListName));		# rejoin $objListName - remove possible spaces
		$symName = "" unless defined($symName);
		$targetDir = ConvertObjdir (($outfile =~ /(\S+(.*\S+)?)\/([^\/]\S+)/)[0]);
		RememberTarget ($symName, $outfile, $type, $objListName, $targetDir);
	} elsif ($line =~ /^mkdir\(.*\)/) {
		ConvertObjdir (($line =~ /\(\s*(\S+(.*\S+)?)\s*\)/)[0]);		#?? not linked to any dependency?
	} elsif ($line =~ /^push\(.*\)/) {
		PushVar(($line =~ /\(\s*(\S+)\s*\)/)[0]);
	} elsif ($line =~ /^pop\(.*\)/) {
		PopVar(($line =~ /\(\s*(\S+)\s*\)/)[0]);
	} else {
		Error ("don't know what to do with line [$line]");
	}
}
#close (IN); -- already closed by preprocessor

Error ("no targets specified") if !@targets;

#------------------------------------------------------------------------------
# makefile generation

print "# Makefile for $COMPILER/$PLATFORM target\n";
print "# This file was automatically generated from \"$INNAME\": do not edit\n\n";

EmitCompilerDefs ();

if (@outdirs2) {
	Splitter ("Directories");
	$i = 0;
	for my $dir (@outdirs) {
		print "${outdirs2[$i]} = $dir\n";
		$i++;
	}
	print "\n";
}


%symTargets = ();
Splitter ("symbolic targets");
# generate "ALL" target (go 1st in makefile) and gather symbolic targets info
print "ALL :";
for $target (@targets) {
	my ($name, $file) = split ('%', $target);
	if ($name ne "") {
		for $item (split(" ", $name)) {
			print " $item" if !exists($symTargets{$item});
			$symTargets{$item} .= " $file";
		}
	} else {
		print " $file";
	}
}
print "\n";
for $target (keys(%symTargets)) {
	print "$target :$symTargets{$target}\n";
}
print "\n";


# generate targets
for $target (@targets) {
	my (undef, $outfile, $type, $objListName, $targetDir) = split('%', $target);
	Splitter("\"$outfile\" target");

	# generate lists obj object files
	for my $listName (split(',', $objListName)) {
		print "${listName}_FILES =".GetObjectList($listName)."\n\n";
	}
	# gather directory list
	my $dirList = GetDirsList($objListName);
	if ($targetDir ne "") {
		$dirList .= " ".$targetDir;
	}

	print FlushTargetText($type, $outfile, $objListName, $dirList)."\n\n";
}

# refine @sources array: remove files which are not linked to any target
@sources2 = ();
for $item (@sources) {
	my ($src, undef, undef, undef, undef, $objListName) = split('%', $item);
	for my $tgt (@targets) {
		my $found = 0;
		my (undef, undef, undef, $tgtObjListName) = split('%', $tgt);
		for my $listName (split(',', $tgtObjListName)) {
			if ($objListName eq $listName) {
				push @sources2, $item;					#?? check for duplicates - single cpp may be included multiple times for different targets
				$found = 1;
			}
		}
		last if $found == 1;
	}
# -- it's normal situation now when the object list is not used
#	WarningOnce("unused object list \"$objListName\"", "unused:$objListName") if defined($item);
}
@sources = @sources2;

#?? print STDERR "Gathering dependency info ...\n";
GatherDependInfo ();
# sort sources array by 1) dependency string, 2) options, etc
# this will produce smaller makefiles (less DEPENDS= and OPTIONS= changes)
@sources = sort {
	my ($srcA, $optA) = $a =~ /^ ([^%]+) % [^%]* % ([^%]* % [^%]* % [^%]* ) % /x;
	my ($srcB, $optB) = $b =~ /^ ([^%]+) % [^%]* % ([^%]* % [^%]* % [^%]* ) % /x;
#	length($depends{$srcA}) <=> length($depends{$srcB})
#							||
			$depends{$srcA} cmp $depends{$srcB}		# sort by dependencies
#							||
#					  $optA cmp $optB				# sort by options
							||
					  $srcA cmp $srcB				# sort by source filename, obj filename
} @sources;

# generate targets for all source files
Splitter ("compiling source files");
%compiled = ();
for $item (@sources) {
	my ($src, $objdir, $defines, $includes, $options, $objListName) = split('%', $item);
	if (!exists($compiled{"$src%$objdir"})) {
		$compiled{"$src%$objdir"} = 1;
		FlushObjectFile ($src, $objdir, $defines, $includes, $options, $objListName);
	}
}

# generate targets for directories
if (@outdirs2) {
	Splitter ("creating output directories");
	for my $dir (@outdirs2) {
		$dir = "\$(".$dir.")";
		print "$dir:\n";
		print "\t", GenMkdir ($dir), "\n\n";
	}
}

#?? print STDERR "Makefile generated.\n";
